<?

/* CharacterModel model class.
 * This class contains data model implementation code.
 *
 */ 
class CharacterModel extends AGModel
{
  function __construct($modelName, &$parent, $charId = null) 
  {
    global $config;
    parent::__construct($modelName);
    
    // for regeneration
    $this->statGain['HP'] = 2;
    $this->castCount = 0;    
    // list all attributes
    $this->attributes = explode(',', 'STR,CON,DEX,INT,WIS,CHA,HP,MP,AP,ARMOR,XP');
    // all attributes that can be increased when levelling up
    foreach(explode(',', 'STR,CON,DEX,INT,WIS,CHA,HP,MP,AP') as $inc)
      $this->increasableAttributes[$inc] = 1;
    // stats that do regenerate over time
    $this->regeneratingStats = explode(',', 'HP,AP,MP');
    // regeneration period
    $this->tickTime = 90;
    
    if ($charId == null)
    {
      // if this is the player character, it gets loaded by session 
      $this->u_key = $parent->user->u_key;
      $this->charDS = $this->loadCharacter();
      if (sizeof($this->charDS) == 0)
      {
        $this->initNewCharacter($this->u_key, $parent->user->localRecord['u_fb_name'], $parent->user->localRecord['u_fb_sex']);
        #AdminNotify('new user: '.$parent->user->localRecord['u_fb_name']);
      }
      $this->initStats();
      if ($this->stats['statincrease'] > 0 || $this->stats['spellincrease'] > 0)
      {
        $config['menu']['character']['caption'] = '<img src="img/icons/edit_add.png" align="absmiddle"/> Character';
      }
    }
    else
    {
      // for any other mob, the character id has to be specified
      $this->charDS = $this->loadCharacter($charId);
      $this->initStats();
    }
    $this->level = $this->charDS['c_level'];
  }
  
  function initNewCharacter($user_key, $char_name, $char_sex)
  {
    $this->charDS = array(
      'c_ref_user' => $user_key,
      'c_name' => $char_name,
      'c_sex' => $char_sex,
      'c_race' => 1,
      );
    CharacterModel::applyRaceToStats($this->stats, 1);
    $this->closeStats();
    $this->saveCharacter();
  }
  
  function makeHit()
  {
    $hit = array();
    if ($this->stats['spell.prepared'] != '' && $this->castCount == 0)
    {
      $spellDS = DB_GetDataset('spells', $this->stats['spell.prepared']);
    }
    if (isset($spellDS) && ($spellDS['sp_level'] <= $this->getCharacterStat('MP') || $this->continueCasting == true))
    {
      $this->castCount++;
      $level = $this->level;
      if ($this->continueCasting != true)
      {
        $mp = $this->getCharacterStat('MP');
        $mp -= $spellDS['sp_level'];
        $this->stats['MP.current'] = $mp;
      }
      ob_start(); eval('?'.'>'.$spellDS['sp_code']); $spellCode = ob_get_clean();
      $hit = TextToStringList($spellCode);
      $maxHit = $this->getCharacterStat('INT') + $this->getCharacterStat('MP.effective') + $this->level;
      $hit['hit'] = $maxHit;
      $hit['spell'] = $spellDS['sp_name'];
      return($hit);
    }
    else
    {
      // make a normal attach
      $maxHit = $this->getCharacterStat('DEX') + $this->level;
      return(array(
        'hit' => $this->rollit($maxHit),
        'dmgtype' => $this->stats['weapon.type'],
        'maxdamage' => $this->getCharacterStat('STR') + $this->level + $this->getCharacterStat('weapon.damage'),
        ));   
    }
  }
  
  function takeHit(&$hitRecord)
  {
    $maxParry = $this->getCharacterStat('DEX') + $this->level;
    $parry = $this->rollit($maxParry);
    return($parry < $hitRecord['hit'] || rand(0, 100) > 30);
  }
  
  function rollit($max)
  {
    $result = 0;
    $div = 8;
    for($a = 0; $a < $div; $a++)
      $result += rand(0, $max);
    return(round($result/$div));
  }
  
  function makeDamage(&$hitRecord)
  {
    $maxDamage = $hitRecord['maxdamage'];
    if (getDefault($hitRecord['damage']) == '')
      $hitRecord['damage'] = $this->rollit($maxDamage) + $this->stats['weapon.magic'];
    else
      $maxDamage = $hitRecord['damage'];
    // critical?
    if (rand(0, 100) < $this->getCharacterStat('DEX') && rand(0,100) < 50) 
    {
      $hitRecord['damage'] = $maxDamage;
      $hitRecord['critical'] = true;
    }    
    return($hitRecord);
  }
  
  function takeDamage(&$hitRecord)
  {
    $attributeDamage = explode(',', getDefault($hitRecord['damage.attribute'], 'HP'));
    // if it's a spell, halved WIS can reduce the damage
    if (isset($hitRecord['spell']))
    {
      $damageReduction = $this->rollit(floor($this->getCharacterStat('WIS')/3));
    }
    // if it's not critical allow CON to buffer the damage
    else //if ($hitRecord['critical'] != true)
      $damageReduction = $this->rollit($this->getCharacterStat('CON'));
      
    // subtract armor protection if it's an actual physical hit
    if ($hitRecord['dmgtype'] != 'force' && $hitRecord['dmgtype'] != 'fire' && $hitRecord['dmgtype'] != 'cold' && $hitRecord['dmgtype'] != 'electricity')
      $damageReduction += $this->getCharacterStat('ARMOR');
    // subtract dmgtype-specific protection
    $hitRecord['armor.special'] = $this->getCharacterStat('ARMOR:'.$hitRecord['dmgtype'], ',.mod');
    $damageReduction += $hitRecord['armor.special'];
    // log the reduction and other stats
    $hitRecord['reducedby'] = $damageReduction;
    $hitRecord['armor'] = $this->getCharacterStat('ARMOR');
    // actually reduce the damage
    $hitRecord['totaldamage'] = $hitRecord['damage'] - $hitRecord['reducedby'];
    if ($hitRecord['totaldamage'] < 0) 
      $hitRecord['totaldamage'] = 0;
    else
      foreach($attributeDamage as $attribute)
      {
        $hitPoints = $this->getCharacterStat('HP');
        $this->setCharacterStat('HP.current', $hitPoints-$hitRecord['totaldamage']);
        $hitRecord['hp'] = $hitPoints-$hitRecord['totaldamage'];
      }
    return($hitRecord['totaldamage']);
  }

  function heal($amount, $stat = 'HP')
  {
    $this->stats[$stat.'.current'] = $this->getCharacterStat($stat);
    $this->stats[$stat.'.current'] += $amount;
    if ($this->stats[$stat.'.current'] > $this->getCharacterStat($stat, ',.effective'))
      unset($this->stats[$stat.'.current']);
  }
  
  function magicBonus($amount, $stat = 'HP')
  {
    if ($this->stats[$stat.'.magic'] < $amount)
      $this->stats[$stat.'.magic'] = $amount;
    $this->recalculateStats();
  }
  
  function encounterAftermath(&$actionDS)
  {
    foreach($this->stats as $k => $v)
    {
      $fs = Cutsegment('.', $k);
      if ($k == 'magic')
      {
        $this->stats[$fs.'.'.$k] -= $actionDS['a_cost'];
        if ($this->stats[$fs.'.'.$k] <= 0)
          unset($this->stats[$fs.'.'.$k]);
      } 
    }
    // adjust AP points for AP cost of that action
    $ap = $this->getCharacterStat('AP');
    $ap -= trim($actionDS['a_cost']);
    $this->stats['AP.current'] = $ap;
    $this->recalculateStats();
  }
  
  function checkStatus()
  {
    return($this->getCharacterStat('HP')+0 > 0);
  }
  
  function unEquipSlot($slotname)
  {
    foreach($this->invList as $invDS)
    {
      if ($invDS['i_slot'] == $slotname) 
        DB_Update('UPDATE '.getTableName('inventory').'
          SET iv_on="N"
          WHERE iv_key="'.$invDS['iv_key'].'"');
    }
  }
  
  function equip($itemId, $mode = 'equip')
  {
    if (!isset($this->invList)) $this->getInventory();
    $eq = $this->invList[$itemId];
    if (sizeof($eq) == 0 || $eq['i_slot'] == '') return;
    $this->unEquipSlot($eq['i_slot']);
    if ($mode == 'equip')
    {
      $ivds = DB_GetDataset('inventory', $eq['iv_key']);
      $ivds['iv_on'] = 'Y';
      DB_UpdateDataset('inventory', $ivds);
    }
    
    $this->recalculateStats();
    $this->saveCharacter();
  }
  
  function addInventoryById($id, $quantity = 1)
  {
    $itemDS = DB_GetDataset('items', $id);
    $this->addInventory($itemDS, $quantity);
  }
  
  function addInventory($itemDS, $quantity = 1)
  {
    // see if we already got one
    $invDS = DB_GetDatasetWQuery('SELECT * FROM '.getTableName('inventory').'
      WHERE iv_character="'.$this->charDS['c_key'].'" AND iv_item="'.$itemDS['i_key'].'"');
    // fill mandatory relationship fields
    $invDS['iv_item'] = $itemDS['i_key'];
    $invDS['iv_character'] = $this->charDS['c_key'];
    $invDS['iv_quantity'] = $invDS['iv_quantity']+$quantity;
    // commit new quantity to inventory
    if ($invDS['iv_quantity'] <= 0)
      DB_Update('DELETE FROM '.getTableName('inventory').' 
        WHERE iv_character="'.$this->charDS['c_key'].'" AND iv_item="'.$itemDS['i_key'].'"');
    else
      DB_UpdateDataset('inventory', $invDS);

    $this->recalculateStats();
    $this->saveCharacter();
  }
  
  function getMoney()
  {
    if (!isset($this->invList)) $this->getInventory();
    return($this->invList[0]['iv_quantity']);
  }
  
  function getInventory()
  {
    $this->invList = array();
    foreach(DB_GetList('SELECT * FROM '.getTableName('inventory').'
      LEFT JOIN '.getTableName('items').' ON (iv_item=i_key)
      WHERE iv_character="'.$this->charDS['c_key'].'"
      ORDER BY i_type DESC,iv_on DESC,i_value DESC') as $invDS)
    {
      $this->invList[$invDS['i_key']] = $invDS;
    }    
    return($this->invList);
  }
  
  function explainItem($itemDS)
  {
    $result = array();
    $props = textToStringList($itemDS['i_properties']);
    foreach($props as $k => $v)
    {
      $type = CutSegment('.', $k); 
      if ($type == 'stats' || $type == 'owner')
      {
        $str = AGController::l10n($k, 'cq').' ';
        if ($v > 0 && substr($v, 0, 1)!='+') $str .= '+';
        $str .= $v;
        $result[] = $str;
      }
    } 
    if ($props['weapon.damage'] != '') $result[] = '+'.$props['weapon.damage'].' '.$props['weapon.type'].' weapon';
    return(implode(' / ', $result));
  }
  
  function applyItemStats($invDS)
  {
    $itemStats = TextToStringList($invDS['i_properties']);
    foreach($itemStats as $k => $v) 
    {
      $fs = CutSegment('.', $k);
      if ($invDS['iv_on'] == 'Y')
      {
        if ($fs == 'stats') $this->stats[$k.'.mod'] += $v;
        if ($fs == 'weapon') $this->stats['weapon.'.$k] = $v;
      }
      if ($fs == 'owner') $this->stats[$k.'.mod'] += $v;
    }
  }

  function recalculateStats($mode = 'normal')
  {
    unset($this->invList);
    $this->getInventory();
    // reset the equipment modifiers
    foreach($this->stats as $k => $v)
    {
      $fs = Cutsegment('.', $k);
      // clear weapon info so we can recalculate later.
      // we don't reset weapon stats for mobs, thereby allowing us to specify
      // virtual weapons in the mob dataset without having to assign real items
      // to the mob
      if ($fs == 'weapon' && $mode != 'mob') unset($this->stats[$fs.'.'.$k]);
      // reset modifiers
      if ($k == 'mod') unset($this->stats[$fs.'.'.$k]);
    }
    // rebuild the equipment modifiers (including new weapon stats)
    foreach($this->invList as $invDS) 
    {
      $this->applyItemStats($invDS);
    }
    // cycle through all of the attributes and decide the *.effective rating
    foreach($this->attributes as $attribute)
    {
      $current = $this->getCharacterStat($attribute);
      $base = $this->stats[$attribute];
      $modifier = $this->stats[$attribute.'.mod'] + 0 + $this->stats[$attribute.'.magic'];
      $effective = $base + $modifier;

      // if it's higher than allowed, unset it
      if ($current > $effective)
        unset($this->stats[$attribute.'.current']); 
        
      // if .current is unnecessary, remove it from the stats
      if ($current == $effective)
        unset($this->stats[$attribute.'.current']);

      if ($modifier == 0)
        // if .effective is unnecessary, remove it from the stats
        unset($this->stats[$attribute.'.effective']);
      else
        // if a modifier exists, use it to determine the .effective rating
        $this->stats[$attribute.'.effective'] = $effective;
    }
  }
  
  function getStat(&$stats, $statname, $steps = ',.effective,.current')
  {
    $result = '';
    foreach(explode(',', $steps) as $suffix)
      if (isset($stats[$statname.$suffix]))
        $result = $stats[$statname.$suffix];
    return($result);
  }
  
  function setStat(&$stats, $statname,  $newValue)
  {
    $stats[$statname] = $newValue;
  }  
  
  function getCharacterStat($statname, $steps = ',.effective,.current')
  {
    if (sizeof($this->stats) == 0) $this->initStats();
    return(0 + CharacterModel::getStat($this->stats, $statname, $steps));
  }
  
  function setCharacterStat($statname, $newValue)
  {
    if (sizeof($this->stats) == 0) $this->initStats();
    CharacterModel::setStat($this->stats, $statname, $newValue);
  }
  
  function saveCharacter()
  {    
    $this->closeStats();
    if ($this->charDS['c_versionflag'] == 1)
    {
      #$this->charDS['c_versionflag'] = 2;
      $defeatDS = DB_GetDatasetMatch('history', array('h_character' => $this->charDS['c_key'],
        'h_type' => 'defeat'));
      $killDS = DB_GetDatasetWQuery('SELECT sum(h_count) FROM `'.getTableName('history').
        '` WHERE h_character="'.$this->charDS['c_key'].'" AND h_type="kill"'); 
      $this->charDS['c_rank_victories'] = $killDS['sum(h_count)'];
      $this->charDS['c_rank_defeats'] = $defeatDS['h_count'];
      @$this->charDS['c_rank_vdratio'] = $killDS['sum(h_count)']/($defeatDS['h_count']+$killDS['sum(h_count)']);
      $partyCountDS = DB_GetDatasetWQuery('SELECT count(*) FROM '.getTableName('parties').'
        WHERE p_party="'.$this->charDS['c_ref_user'].'"');
      $this->charDS['c_rank_party'] = $partyCountDS['count(*)'];
    }
    $this->charDS['c_rank_xp_earned'] = $this->stats['XP'];
    $this->charDS['c_rank_gold'] = $this->getMoney();
    $this->charDS['c_lastupdate'] = date('Y-m-d H:i:s');
    $this->charDS['c_key'] = DB_UpdateDataset('characters', $this->charDS);
  }
  
  function loadCharacter($c_key = null)
  {
    if ($c_key == null)
      return(DB_GetDataset('characters', $this->u_key, 'c_ref_user'));
    else
      return(DB_GetDataset('characters', $c_key));
  }
  
  function regenerate($tickCount)
  {
    foreach($this->regeneratingStats as $stat)
    {
      $max = $this->getCharacterStat($stat, ',.effective');
      $current = $this->getCharacterStat($stat);
      $regen = $this->getCharacterStat($stat.'.regeneration') + getDefault($this->statGain[$stat], 1);
      $amount = $tickCount*$regen;
      if ($amount > $max - $current)
        $this->stats[$stat.'.current'] = $max;
      else
        $this->stats[$stat.'.current'] += $amount;
    }    
    DB_Update('UPDATE '.getTableName('parties').'
      SET p_hp = p_hp + '.($tickCount*5).'
      WHERE p_party="'.$this->charDS['c_ref_user'].'" AND p_group="Y" 
      ');
    DB_Update('UPDATE '.getTableName('parties').'
      SET p_hp = p_hp_max
      WHERE p_party="'.$this->charDS['c_ref_user'].'" AND p_group="Y" AND p_hp > p_hp_max 
      ');
  }
  
  function learnSpell($spell_id)
  {
    $spellDS = DB_GetDataset('spells', $spell_id);
    $existingDS = DB_GetdatasetMatch('character_spells', array(
      'cs_character' => $this->charDS['c_key'],
      'cs_spell' => $spell_id)); 
    if ($this->stats['spellincrease'] > 0 
      && !isset($existingDS['cs_key']) 
      && $this->level >= $spellDS['sp_level'])
    {
      $this->stats['spellcount']++;
      $this->stats['spellincrease']--;
      DB_UpdateDataset('character_spells', $existingDS);
      $this->saveCharacter();
    }
  }
  
  function castSpell($spell_id)
  {
    $spellDS = DB_GetDataset('spells', $spell_id);
    $existingDS = DB_GetdatasetMatch('character_spells', array(
      'cs_character' => $this->charDS['c_key'],
      'cs_spell' => $spell_id));
    $currentMP = $this->getCharacterStat('MP'); 
    $currentAP = $this->getCharacterStat('AP');
    if (!isset($existingDS['cs_key'])) 
      logError('You cannot cast that spell');
    else if ($currentMP < $spellDS['sp_level'])
      logError('You do not have enough MP to cast that spell');
    else if ($currentAP < $spellDS['sp_level'])
      logError('You do not have enough AP to cast that spell');
    else
    {
      eval($spellDS['sp_code']);
      $currentMP -= $spellDS['sp_level'];
      $currentAP -= floor($spellDS['sp_level']/2);
      $this->stats['AP.current'] = $currentAP;
      $this->stats['MP.current'] = $currentMP;
      $this->recalculateStats();
      $this->saveCharacter();
    }
  }

  function levelUp()
  {
    $this->charDS['c_level']++;
    $this->stats['HP'] += 2;
    $this->stats['AP']++;
    $this->stats['statincrease'] += 3;
    $this->stats['skillincrease'] += 5;
    $this->heal($this->charDS['c_level']*2, 'HP');
    $this->heal($this->charDS['c_level'], 'AP');
    $this->message = '<img src="img/icons/smile.png" align="left" style="padding-right: 8px;"/>
      <b>You are now level '.$this->charDS['c_level'].', congratulations!</b><br/>
      Buff up on your <a href="'.actionUrl('index', 'character').'">character profile</a>!<br/>&nbsp;';
    #AdminNotify('level up: '.$this->charDS['c_name'].' ('.$this->charDS['c_level'].')');
  }
  
  function IncreaseStat($stat)
  {
    $this->initStats();
    if ($this->stats['statincrease'] > 0 && $this->increasableAttributes[$stat] > 0)
    {
      $this->stats['statincrease']--;
      $this->stats[$stat] += $this->increasableAttributes[$stat];
      $this->recalculateStats();
      $this->closeStats();
      $this->saveCharacter();
    }
  }
  
  function closeStats()
  {
    $this->packStats($this->charDS, $this->stats);
  }

  function extractStats(&$charDS, $field = 'c_stats')
  {
    return(TextToStringList($charDS[$field]));
  }
  
  function packStats(&$charDS, &$stats)
  {
    ksort($stats);
    $charDS['c_stats'] = StringListToText($stats);    
  }

  function initStats()
  {
    if (isset($this->stats)) return;
    $this->stats = $this->extractStats($this->charDS);
    $this->name = $this->charDS['c_name'];
    
    if ($this->stats['MP'] > 0)
      $potentialSpellCount = 1+floor($this->stats['MP']/3);
    else
      $potentialSpellCount = 0;
    
    $spellIncrease = $potentialSpellCount - $this->stats['spellcount'];
    if ($spellIncrease != $this->stats['spellincrease'])
    {
      $this->stats['spellincrease'] = $spellIncrease;
      $this->saveCharacter();
    }
    
    if ($this->charDS['c_ref_user'] != 0) $this->handleTick();
  }
  
  function calcXPGoal($toLevel)
  {
    $toLevel++;
    return(pow($toLevel, 3));
  }
  
  function handleTick()
  {
    $dirtyFlag = false;
    // see if it's time to regenerate
    $passedSeconds = time() - $this->stats['tick'];
    if ($passedSeconds > $this->tickTime)
    {
      $rest = $passedSeconds % $this->tickTime;
      $this->regenerate(floor($passedSeconds / $this->tickTime));
      $this->stats['tick'] = time() - $rest;
      $dirtyFlag = true;
    }
    else
      $this->timeToNextTick = time() - $this->stats['tick'];
    // see if we gained a level
    $this->xpGoal = $this->calcXPGoal($this->charDS['c_level']);
    if ($this->charDS['c_level'] > 1)
      $this->prevXpGoal = $this->calcXPGoal($this->charDS['c_level']-1);
    else
      $this->prevXpGoal = 0;
    
    if ($this->getCharacterStat('XP') >= $this->xpGoal)
    {
      $this->levelUp();
      $dirtyFlag = true;
    }
    // if anything changed, we need to save
    if ($dirtyFlag)
    {
      $this->closeStats();
      $this->saveCharacter();
    }
  }
  
  function applyRaceToStats(&$stats, $raceId)
  {
    $raceDS = DB_GetDataset('races', $raceId);
    $raceStats = CharacterModel::extractStats($raceDS, 'r_stats');
    foreach($raceStats as $k => $v)
    {
      if (!isset($stats[$k])) $stats[$k] = $v;
    }
  }

  function addHistoryKill(&$mobObject, &$action)
  {
    $historyDS = DB_GetDatasetWQuery('SELECT * FROM '.getTableName('history').'
      WHERE h_character="'.$this->charDS['c_key'].'" AND h_type="kill" AND h_mob="'.$mobObject->charDS['c_key'].'"');
    if (sizeof($historyDS) == 0)
      $historyDS = array(
        'h_character' => $this->charDS['c_key'],
        'h_type' => 'kill',
        'h_mob' => $mobObject->charDS['c_key'],
        'h_first' => date('Y-m-d H:i:s'),
        );
    $historyDS['h_last'] = date('Y-m-d H:i:s');
    $historyDS['h_count']++;
    DB_UpdateDataset('history', $historyDS);
  }

}


?>
